/**
*mini 版的 webpack 打包流程
*1）从入口文件开始解析
*2）查找入口文件引入了哪些 js 文件，找到依赖关系
*3）递归遍历引入的其他 js，生成最终的依赖关系图谱
*4）同时将 ES6 语法转化成 ES5
*5）最终生成一个可以在浏览器加载执行的 js 文件
*/
const fs = require('fs')
const path = require('path')

//babylon解析js语法，生成 AST 语法树
//AST 将js代码转化为一种 JSON 数据结构

const babylon = require('babylon')
//babel-traverse 是一个对 AST 进行遍历的工具，对 AST 进行替换
const traverse = require('babel-traverse').default

//将 es6 es7 等高级语法转化为 es5 的语法
const { transformFromAst } = require('babel-core')

//将控制台输出导出到文件，方便查看各个抽象概念
const log = fs.createWriteStream("./console.log")
log.once('open', () => {
    console.log(`开始打包，${log.logFrom} log 输入流打开`)
})

log.once('close', () => {
    console.log(`打包结束，log 输入流关闭`)
})

//控制台格式化输出
const logFormat = (tip, tag, value) => {
    if (tip && !tag && !value) {
        return `------${tip}------\n`
    }
    return `{\n${tag}:" ${value}\n}\n`
}

//每一个 js 文件，对应一个 id
let ID = 0

//filename 参数为文件路径，读取内容并提取它的依赖关系
function createAsset(filename) {
    log.write(logFormat("createAsset()"))
    const content = fs.readFileSync(filename, 'utf-8')

    log.write(logFormat(null, "content", content))
    //获取该文件对应的 AST 抽象语法树
    const ast = babylon.parse(content, {
        sourceType: 'module'
    })

    log.write(logFormat("ast", ast))

    //dependencies 保存所依赖的模块的相对路径
    const dependencies = []


    //通过查找 import 节点，找到该文件的依赖关系
    //因为项目中一般通过 import 引入其他文件，找到了 import 节点，就找到了这个文件引用了哪些文件
    traverse(ast, {
        ImportDeclaration: ({ node }) => {
            //查找 import 节点
            dependencies.push(node.source.value)
        }
    })

    log.write(logFormat("dependencies", dependencies));

    //通过递增计数器，为此模块分配唯一标识符，用于缓存已解析过的文件
    const id = ID++
    //该 `presets` 选项是一组规则，告诉 `babel` 如何传输我们的代码
    //用 `babel-preset-env` 将代码转换为浏览器可以运行的东西
    const { code } = transformFromAst(ast, null, {
        presets: ['env']
    })

    log.write(logFormat("code", code))

    //返回此模块的相关信息
    return {
        id,//文件id，唯一
        filename,//文件路径
        dependencies,//文件的依赖关系
        code //文件代码
    }
}

//提取每一个依赖文件的依赖关系，循环下去：找到对应这个项目的 `依赖图`
function createGraph(entry) {
    //开始打包时打开输入流
    log.open()
    log.write(logFormat("createGraph()"))
    //得到入口文件的依赖关系
    const mainAsset = createAsset(entry)
    log.write(logFormat("mainAsset", mainAsset))
    const queue = [mainAsset]
    for (const asset of queue) {
        //记录依赖文件的文件ID
        asset.mapping = {}
        //获取这个模块所在的目录
        const dirname = path.dirname(asset.filename)
        log.write(logFormat("dirname", dirname))
        asset.dependencies.forEach((relativePath) => {
            //通过将相对路径与父资源目录的路径连接，将相对路径转变为绝对路径
            //每个文件的绝对路径是固定的、唯一的
            const absolutPath = path.join(dirname, relativePath)
            log.write(logFormat("absolutPath", absolutPath))
            //递归解析其中引入的其他资源
            const child = createAsset(absolutPath)
            log.write(logFormat("child", child))
            asset.mapping[relativePath] = child.id
            //将 `child` 推入队列，通过递归实现了依赖关系解析
            queue.push(child)
        })
    }
    //queue就是最终的依赖关系图谱
    return queue
}

//自定义实现了 require 方法，找到导出变量的引用逻辑
function bundle(graph) {
    log.write(logFormat("bundle()"))
    let modules = ''
    graph.forEach((mod) => {
        modules += `${mod.id}: [
            function (require,module,exports) {${mod.code}},
            ${JSON.stringify(mod.mapping)}     
        ],`
        log.write(logFormat("modules", modules))
    })

    const result = `
        (function (modules) {
            function require(id) {
                const [fn, mapping] = modules[id]
                function localRequire(name) {
                    return require(mapping[name])
                }

                const module = { exports: {} }
                fn(localRequire, module, module.exports)
                return module.exports
            }
            require(0)
        })({ ${modules}})`
    return result
}

//项目的入口文件
const graph = createGraph('./test/MiniWebPackEntry.js')
log.write(logFormat("graph", graph))
const result = bundle(graph)
log.write(logFormat("result", result))
//创建 dist 目录，将打包内容写入 main.js 中


fs.mkdir('dist', (err) => {
    if (!err) {
        fs.writeFile('./dist/main.js', result, (error) => {
            if (!error) {
                console.log("打包成功！");
                //关闭 log 文件输入流
                log.close()
            } else {
                console.log({ error });
            }
        })
    } else {
        console.log({ err });
        log.close()
    }
})
